﻿namespace NTLib.Core.Log
{
    using Configuration;
    using Configuration.Attributes;
    using Configuration.Values;
    using NTLib.Core.Extensions;
    using NTLib.Core.Reflection;
    using NTLib.Core.Threading;
    using Pool;
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Threading;
    using System.Threading.Tasks;

    /// <summary>
    /// 
    /// </summary>
    /// <seealso cref="NTLib.Core.Configuration.BaseConfigurable" />
    // TODO : Import and create log converter From exception to string example
    [TypeImportConfiguration(typeof(ILogConsumer))]
    public sealed class NTLog : BaseConfigurable
    {
        #region Fields

        private static readonly NTLog s_instance = new NTLog();

        private readonly List<Guid> _consumerByConfig;
        private readonly ObjectPool<LogContainer> _pool;
        private readonly Dictionary<Guid, ILogConsumer> _consumers;
        private readonly List<ILogConsumer> _consumerList;
        private readonly Queue<LogContainer> _logsToTreat;
        private readonly Queue<LogContainer> _logsToConsum;
        private readonly SemaphoreSlim _logLock;
        private readonly SemaphoreSlim _sealedLock;
        private readonly SemaphoreSlim _logLockConsum;
        private readonly ReaderWriterLockSlim _consumerLock;
        private CancellationTokenSource _cleanTokenSource;
        private CancellationToken _cleanToken;
        private Task _consumerTask;
        private ReadOnlyCollection<ILogConsumer> _roConsumer;

        #endregion

        #region Ctor

        /// <summary>
        /// Initializes the <see cref="NTLog"/> class.
        /// </summary>
        /// <remarks>The ctor is in internal and not in private to allow unit test to create logger individualy on each test.</remarks>
        internal NTLog()
            : base(true)
        {
            this._consumerByConfig = new List<Guid>();
            _pool = new ObjectPool<LogContainer>();
            _consumers = new Dictionary<Guid, ILogConsumer>();
            _consumerLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
            _sealedLock = new SemaphoreSlim(1);
            _logsToTreat = new Queue<LogContainer>();
            _logsToConsum = new Queue<LogContainer>();
            _logLock = new SemaphoreSlim(1);
            _logLockConsum = new SemaphoreSlim(1);
            _cleanTokenSource = new CancellationTokenSource();
            _cleanToken = _cleanTokenSource.Token;
            _consumerList = new List<ILogConsumer>();
            _roConsumer = new ReadOnlyCollection<ILogConsumer>(_consumerList);

            _consumerTask = ConsumeLog();

#if DEBUG
            LogLevel = LogType.Error | LogType.Warning | LogType.Other | LogType.Information;
#else
            LogLevel = LogType.Error | LogType.Other;
#endif
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the log level.
        /// </summary>
        /// <value>
        /// The log level.
        /// </value>
        public LogType LogLevel { get; set; }

        /// <summary>
        /// Gets the instance.
        /// </summary>
        public static NTLog Instance
        {
            get
            {
                try
                {
                    if (s_instance.IsConfigured == false)
                    {
                        ConfigManager.Configure(s_instance);
                    }
                }
                catch
                {

                }
                return s_instance;
            }
        }

        /// <summary>
        /// Gets the consumers.
        /// </summary>
        public IReadOnlyCollection<ILogConsumer> Consumers
        {
            get
            {
                using (_consumerLock.LockRead(_cleanToken))
                {
                    return _roConsumer;
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Clears this instance.
        /// </summary>
        /// <param name="clearConsumer">If true, this method will clear all the consumer instance.</param>
        /// <remarks>Attention if it stay some log to manage, they will will be destroyed. You must called the flush method to clean properly.</remarks>
        public void Clear(bool clearConsumer = true)
        {
            using (this._sealedLock.ScopeWait())
            {
                _cleanTokenSource.Cancel();
                _cleanTokenSource = new CancellationTokenSource();
                _cleanToken = _cleanTokenSource.Token;

                Task taskToWait = null;

                using (_logLock.ScopeWait())
                {
                    using (_logLockConsum.ScopeWait())
                    {
                        this._logsToConsum.SafeClear();
                        this._logsToTreat.SafeClear();

                        taskToWait = this._consumerTask;
                    }
                }

                if (taskToWait != null)
                    taskToWait.Wait(TimeSpan.FromSeconds(30));

                if (clearConsumer)
                {
                    using (this._consumerLock.LockWrite())
                    {
                        List<Task> consumerFlush = new List<Task>();
                        foreach (var consumer in this._consumers)
                        {
                            consumerFlush.Add(consumer.Value.FlushAsync());
                        }

                        Task.WaitAll(consumerFlush.ToArray());

                        this._consumerList.Clear();
                        this._consumers.Clear();
                        this._consumerByConfig.Clear();
                    }
                }
            }
        }

        /// <summary>
        /// Registers the consumer.
        /// </summary>
        /// <param name="consumer">The consumer.</param>
        public Guid RegisterConsumer(ILogConsumer consumer)
        {
            Guid key = Guid.NewGuid();

            using (_consumerLock.LockWrite(_cleanToken))
            {
                this._consumers.Add(key, consumer);
                this._consumerList.Add(consumer);
            }
            return key;
        }

        /// <summary>
        /// Uns the register consumer.
        /// </summary>
        /// <param name="key">The key.</param>
        public void UnRegisterConsumer(Guid key)
        {
            using (_consumerLock.LockWrite(_cleanToken))
            {
                ILogConsumer consumer = null;
                if (this._consumers.TryGetValue(key, out consumer))
                {
                    this._consumers.Remove(key);
                    this._consumerList.Remove(consumer);
                }
            }
        }

        /// <summary>
        /// Lock the instance and flush all the log not managed.
        /// </summary>
        public async Task FlushAsync()
        {
            using (await this._sealedLock.ScopeWaitAsync(_cleanToken))
            {
                bool anyLogToProcess = false;
                do
                {
                    await ConsumeAsync();
                    Task task = null;

                    using (await _logLockConsum.ScopeWaitAsync(_cleanToken))
                    {
                        anyLogToProcess = (this._logsToTreat.Count > 0 || this._logsToConsum.Count > 0);

                        if (this._consumerTask == null && anyLogToProcess)
                            this._consumerTask = ConsumeLog();
                        task = this._consumerTask;
                    }

                    if (task != null)
                        await task;

                } while (anyLogToProcess);

                using (this._consumerLock.LockRead())
                {
                    List<Task> consumerFlush = new List<Task>();
                    foreach (var consumer in this._consumers)
                    {
                        consumerFlush.Add(consumer.Value.FlushAsync());
                    }

                    await Task.WhenAll(consumerFlush);
                }
            }
        }

        #region Logs

        /// <summary>
        /// the execute the action with a try catch around where the exception is stored in the NTLog.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="origin">The origin.</param>
        /// <param name="context">The context.</param>
        /// <param name="emetor">The emetor.</param>
        public void SafeExecute(Action action, string origin = null, object context = null, [CallerMemberName]string emetor = null)
        {
            try
            {
                action();
            }
            catch (Exception ex)
            {
                RecordError(ex, origin, context, emetor);
            }
        }

        /// <summary>
        /// Records the specified log, type information.
        /// </summary>
        /// <typeparam name="T">Type of the data log</typeparam>
        /// <param name="data">The data log.</param>
        /// <param name="origin">The origin of the log, MUST be the class/structure or assembly name.</param>
        /// <param name="context">The context of the log.</param>
        /// <param name="emetor">The emetor of the log, by default it use the CallMemberName value.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void RecordInformation<T>(T data, string origin = null, object context = null, [CallerMemberName]string emetor = null)
            where T : class
        {
            Record(LogType.Information, data, origin, context, emetor);
        }

        /// <summary>
        /// Records the specified log, type warning.
        /// </summary>
        /// <typeparam name="T">Type of the data log</typeparam>
        /// <param name="data">The data log.</param>
        /// <param name="origin">The origin of the log, MUST be the class/structure or assembly name.</param>
        /// <param name="context">The context of the log.</param>
        /// <param name="emetor">The emetor of the log, by default it use the CallMemberName value.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void RecordWarning<T>(T data, string origin = null, object context = null, [CallerMemberName]string emetor = null)
            where T : class
        {
            Record(LogType.Warning, data, origin, context, emetor);
        }

        /// <summary>
        /// Records the specified log, type error.
        /// </summary>
        /// <typeparam name="T">Type of the data log</typeparam>
        /// <param name="data">The data log.</param>
        /// <param name="origin">The origin of the log, MUST be the class/structure or assembly name.</param>
        /// <param name="context">The context of the log.</param>
        /// <param name="emetor">The emetor of the log, by default it use the CallMemberName value.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void RecordError<T>(T data, string origin = null, object context = null, [CallerMemberName]string emetor = null)
            where T : class
        {
            Record(LogType.Error, data, origin, context, emetor);
        }

        /// <summary>
        /// Records the specified log, type other.
        /// </summary>
        /// <typeparam name="T">Type of the data log</typeparam>
        /// <param name="data">The data log.</param>
        /// <param name="origin">The origin of the log, MUST be the class/structure or assembly name.</param>
        /// <param name="context">The context of the log.</param>
        /// <param name="emetor">The emetor of the log, by default it use the CallMemberName value.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void RecordOther<T>(T data, string origin = null, object context = null, [CallerMemberName]string emetor = null)
            where T : class
        {
            Record(LogType.Other, data, origin, context, emetor);
        }

        /// <summary>
        /// Records the specified log.
        /// </summary>
        /// <typeparam name="T">Type of the data log</typeparam>
        /// <param name="type">The type of the lof.</param>
        /// <param name="data">The data log.</param>
        /// <param name="origin">The origin of the log, MUST be the class/structure or assembly name.</param>
        /// <param name="context">The context of the log.</param>
        /// <param name="emetor">The emetor of the log, by default it use the CallMemberName value.</param>
        public async void Record<T>(LogType type, T data, string origin = null, object context = null, [CallerMemberName]string emetor = null)
            where T : class
        {
            if ((type & LogLevel) != type)
                return;

            using (_consumerLock.LockRead(_cleanToken))
            {
                if (this._consumerList.Count == 0)
                    return;
            }

            using (await this._sealedLock.ScopeWaitAsync(_cleanToken))
            {
                var container = _pool.Get();
                container.Initialize(type, origin, emetor, data, context);

                using (await _logLock.ScopeWaitAsync(_cleanToken))
                {
                    _logsToTreat.Enqueue(container);
                }

                await ConsumeAsync(_cleanToken);
            }
        }

        #endregion

        /// <summary>
        /// Called to Loads the configuration.
        /// </summary>
        /// <param name="configs">The configuration values.</param>
        protected override void OnLoadConfig(ConfigurationCollection configs)
        {
            var ilogConsumer = typeof(ILogConsumer);
            foreach (var cf in configs)
            {
                var config = cf.Value.FirstOrDefault() as TypeConfig;
                if (config != null && config.Value.IsOrInheritOf(ilogConsumer))
                {
                    this._consumerByConfig.Add(this.RegisterConsumer(Activator.CreateInstance(config.Value) as ILogConsumer));
                }
            }
        }

        /// <summary>
        /// Called to reset the configuration.
        /// </summary>
        protected override void OnResetConfig()
        {
            foreach (var id in this._consumerByConfig)
            {
                this.UnRegisterConsumer(id);
            }
        }

        /// <summary>
        /// Consume all the logs
        /// </summary>
        private async Task ConsumeAsync(CancellationToken token = default(CancellationToken))
        {
            token.ThrowIfCancellationRequested();
            _cleanToken.ThrowIfCancellationRequested();
            using (await _logLock.ScopeWaitAsync(_cleanToken))
            {
                token.ThrowIfCancellationRequested();
                _cleanToken.ThrowIfCancellationRequested();

                using (await _logLockConsum.ScopeWaitAsync(_cleanToken))
                {
                    token.ThrowIfCancellationRequested();
                    _cleanToken.ThrowIfCancellationRequested();
                    while (_logsToTreat.Count > 0)
                    {
                        token.ThrowIfCancellationRequested();
                        _cleanToken.ThrowIfCancellationRequested();
                        _logsToConsum.Enqueue(_logsToTreat.Dequeue());
                    }

                    if (this._consumerTask == null)
                        this._consumerTask = ConsumeLog();
                }
            }
        }

        /// <summary>
        /// Consumes the logs.
        /// </summary>
        /// <returns></returns>
        private async Task ConsumeLog()
        {
            try
            {
                while (!this._cleanToken.IsCancellationRequested)
                {
                    Queue<LogContainer> localInfo = null;
                    using (await _logLockConsum.ScopeWaitAsync(_cleanToken))
                    {
                        // Break the loop if no log have to be consume
                        if (!_logsToConsum.Any())
                        {
                            _consumerTask = null;
                            return;
                        }
                        localInfo = new Queue<LogContainer>(this._logsToConsum);
                        _logsToConsum.Clear();
                    }

                    while (localInfo.Any())
                    {
                        _cleanToken.ThrowIfCancellationRequested();
                        using (var log = localInfo.Dequeue())
                        {
                            using (_consumerLock.LockRead(_cleanToken))
                            {
                                foreach (var consumer in _consumerList)
                                {
                                    _cleanToken.ThrowIfCancellationRequested();
                                    try
                                    {
                                        if (consumer.FilterTypes != null && !consumer.FilterTypes.Any(f => f == log.DataType))
                                            continue;
                                        if (consumer.FilterContextTypes != null && !consumer.FilterContextTypes.Any(f => f == log.ContextType))
                                            continue;

                                        consumer.Log(log);
                                    }
                                    catch (Exception ex)
                                    {
                                        Debug.WriteLine("NTLog : consumer {0} error -> {1}", consumer.GetType(), ex.Message);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                RecordError(ex, nameof(NTLog));
                using (_logLockConsum.ScopeWait())
                {
                    if (!_logsToConsum.Any())
                        _consumerTask = ConsumeLog();
                    else
                        _consumerTask = null;
                }
            }
        }

        #endregion
    }
}
